#include "../config.h"

#include <glibmm/i18n.h>
#ifdef USE_VORBIS
# include <vorbis/codec.h>
# include <vorbis/vorbisfile.h>
#endif
#include <ao/ao.h>
#include <string.h>
#include <math.h>
#include <boost/utility.hpp>

#include "log.h"
#include "application.h"
#include "screensaver.h"
#include "sound.h"

namespace gpyp {
namespace sound {

class Player;
typedef boost::ptr_list<Player> PlayerList;

namespace {
Application      *_app = NULL;
SlotError         _error_cb;
SlotComplete      _complete_cb;
Glib::ustring     _error_message;
Glib::Dispatcher *_dispatcher_error    = NULL;
Glib::Dispatcher *_dispatcher_complete = NULL;

Glib::StaticMutex _mutex = GLIBMM_STATIC_MUTEX_INIT;
PlayerList       *_players = NULL;
}

class Player : boost::noncopyable {
private:
  static uint _id;

private:
  Application  &app_;
  uint          id_;
  Glib::ustring file_path_;
  double        volume_;

  bool          stop_;

private:
  inline int get_driver() {
    const Glib::ustring &driver_name = *app_.conf().sound()->driver();
    if (driver_name.empty()) {
      return ao_default_driver_id();
    } else {
      return ao_driver_id(driver_name.c_str());
    }
  }

  inline static ao_option *create_option(const Glib::ustring &key,
                                         const Glib::ustring &value,
                                         ao_option           *next) {
    ao_option *opt = new ao_option();
    opt->key   = g_strdup(key.c_str());
    opt->value = g_strdup(value.c_str());
    opt->next  = next;
    return opt;
  }
  inline static void delete_option(ao_option *option) {
    while(option) {
      ao_option *next = option->next;
      g_free(option->key);
      g_free(option->value);
      delete option;
      option = next;
    }
  }

  inline ao_option *generate_options() {
    ConfMain::RefSound sound = app_.conf().sound();
    const Glib::ustring &driver_name = *sound->driver();
    ao_option *options = NULL;
    if (driver_name == "alsa") {
      const Glib::ustring &card     = *sound->alsa()->card();
      const Glib::ustring &dev      = *sound->alsa()->dev();
      const Glib::ustring &buf_size = *sound->alsa()->buf_size();
      if (!buf_size.empty()) {
        options = create_option("buf_size", buf_size, options);
      }
      if (!dev.empty()) {
        options = create_option("dev", dev, options);
      }
      if (!card.empty()) {
        options = create_option("card", card, options);
      }
    } else if (driver_name == "pulse") {
      const Glib::ustring &server = *sound->pulse()->server();
      const Glib::ustring &sink   = *sound->pulse()->sink();
      if (!sink.empty()) {
        options = create_option("sink", sink, options);
      }
      if (!server.empty()) {
        options = create_option("server", server, options);
      }
    } else if (driver_name == "esd") {
      const Glib::ustring &host = *sound->esd()->host();
      if (!host.empty()) {
        options = create_option("host", host, options);
      }
    } else if (driver_name == "oss") {
      const Glib::ustring &dsp = *sound->oss()->dsp();
      if (!dsp.empty()) {
        options = create_option("dsp", dsp, options);
      }
    }
    return options;
  }

protected:
  virtual bool prepare_file(const Glib::ustring &file_path,
                            Glib::ustring       &error_message) = 0;
  virtual void prepare_format(ao_sample_format &format) = 0;
  virtual long read(char *buf, size_t size) = 0;
  virtual void volume(ao_sample_format &format,
                      char *buf, size_t size,
                      double volume) = 0;
  virtual void finalize() = 0;

public:
  inline bool play(Glib::ustring &error_message) {
    stop_ = false;

    int driver = get_driver();
    if (driver < 0) {
      error_message = _("Failed to find a usable audio output device");
      return false;
    }

    try {
      if (!prepare_file(file_path_, error_message)) {
        return false;
      }
    } catch(const Glib::Exception &ex) {
      log::err() << ex.what() << log::endl;
      return false;
    }

    ao_sample_format format;
    memset(&format, 0, sizeof(format));
    prepare_format(format);
    ao_option *options = generate_options();
    ao_device *device = ao_open_live(driver, &format, options);
    delete_option(options);
    if (device) {
      // for 0.2 sec
      size_t bufsize = format.bits * format.channels * format.rate / 8 / 5;
      char *buffer = new char[bufsize];

      while(!stop_) {
        long ret = read(buffer, bufsize);
        if (ret == 0) {
          // EOF
          break;
        } else if (ret < 0) {
          // error
        } else {
          volume(format, buffer, ret, volume_);
          ao_play(device, buffer, ret);
        }
      }
      delete [] buffer;
      ao_close(device);
      finalize();
      return true;
    } else {
      error_message = _("Failed to open audio output device");
      finalize();
      return false;
    }
  }

  inline void stop() {
    stop_ = true;
  }

  inline uint get_id() const {
    return id_;
  }

  inline Player(Application         &app,
                const Glib::ustring &file_path,
                double               volume) :
    app_      (app),
    id_       (++_id),
    file_path_(file_path),
    volume_   (0),
    stop_     (false)
  {
    // 20log[10]volume = 30v - 30(dB)
    if (volume < 0) volume = 0;
    if (volume > 1) volume = 1;
    volume_ = volume == 0 ? 0 : pow(10, (30 * volume - 30) / 20);
  }
  virtual ~Player() {
  }
};

uint Player::_id = 0;

#define CALC_SIZE_2(C) (((C)[0] & 0xff) | (((C)[1] << 8) & 0xff00))
#define CALC_SIZE_4(C) (((C)[0] & 0xff) |               \
                        (((C)[1] << 8) & 0xff00) |      \
                        (((C)[2] << 16) & 0xff0000) |   \
                        (((C)[3] << 24) & 0xff000000))
class WavPlayer : public Player {
private:
  typedef struct {
    char riff[4];
    char size[4];
    char wave[4];
  } WaveHeader;
  typedef struct {
    char          name[4];
    unsigned char size[4];
  } Chunk;
  typedef struct {
    unsigned char tag[2];
    unsigned char channels[2];
    unsigned char samples_per_sec[4];
    unsigned char avg_bytes_per_sec[4];
    unsigned char block_align[2];
    unsigned char bits_per_sample[2];
  } WaveFormat;

  FILE      *file_;
  WaveFormat format_;

protected:
  virtual bool prepare_file(const Glib::ustring &file_path,
                            Glib::ustring       &error_message) {
    std::string path = Glib::filename_from_utf8(file_path);
    file_ = fopen(path.c_str(), "r");
    if (!file_) {
      error_message = _("failed to open file");
      return false;
    }

    long fmt_offset = -1;
    long data_offset = -1;
    WaveHeader wavehead;
    if (fread(&wavehead, sizeof(wavehead), 1, file_) != 1 ||
        strncmp(wavehead.riff, "RIFF", 4) ||
        strncmp(wavehead.wave, "WAVE", 4)) {
      error_message = _("file is not wave format");
      goto err;
    }

    while(!feof(file_)) {
      Chunk chunk;
      if (fread(&chunk, sizeof(chunk), 1, file_) != 1) break;
      uint32_t chunk_size = CALC_SIZE_4(chunk.size);
      if (!strncmp(chunk.name, "fmt ", 4)) {
        fmt_offset = ftell(file_);
        if (fread(&format_, sizeof(format_), 1, file_) != 1) {
          error_message = _("failed to read format");
          goto err;
        }
        if (CALC_SIZE_2(format_.tag) != 1) {
          error_message = _("unsupported format");
          goto err;
        }
        fseek(file_, fmt_offset, SEEK_SET);
      } else if (!strncmp(chunk.name, "data", 4)) {
        data_offset = ftell(file_);
      }
      if (fseek(file_, chunk_size, SEEK_CUR)) {
        error_message = _("failed to seek");
        goto err;
      }
    }
    if (fmt_offset < 0) {
      error_message = _("there is no fmt chunk");
      goto err;
    }
    if (data_offset < 0) {
      error_message = _("there is no data chunk");
      goto err;
    } else if (fseek(file_, data_offset, SEEK_SET)) {
      log::log() << "data: " << data_offset << log::endl;
      error_message = _("failed to seek");
      goto err;
    }
    return true;

  err:
    fclose(file_);
    file_ = NULL;
    return false;
  }
  virtual void prepare_format(ao_sample_format &format) {
    format.bits        = CALC_SIZE_2(format_.bits_per_sample);
    format.rate        = CALC_SIZE_4(format_.samples_per_sec);
    format.channels    = CALC_SIZE_2(format_.channels);
    format.byte_format = AO_FMT_LITTLE;
  }
  virtual long read(char *buf, size_t size) {
    if (feof(file_) || ferror(file_)) {
      return 0;
    } else {
      return fread(buf, 1, size, file_);
    }
  }
  virtual void volume(ao_sample_format &format,
                      char *buf, size_t size,
                      double volume) {
    switch(format.bits) {
    case 8:
      for(size_t i = 0; i < size; ++i) {
        uint8_t *p = (uint8_t *)(buf + i);
        *p = static_cast<uint8_t>((*p - 128) * volume + 128);
      }
      break;
    case 16:
      for(size_t i = 0; i < size; i += 2) {
        unsigned char *p = (unsigned char *)(buf + i);
        int16_t s = (p[0] & 0xff) | ((p[1] << 8) & 0xff00);
        s = static_cast<int16_t>(s * volume);
        p[0] = s & 0xff;
        p[1] = (s >> 8) & 0xff;
      }
      break;
    }
  }
  virtual void finalize() {
    fclose(file_);
    file_ = NULL;
  }

public:
  WavPlayer(Application         &app,
            const Glib::ustring &file_path,
            double               volume) :
    Player(app, file_path, volume),
    file_ (NULL)
  {
  }
};

#ifdef USE_VORBIS
class OggPlayer : public Player {
private:
  OggVorbis_File vfile_;
  int current_section_;

protected:
  virtual bool prepare_file(const Glib::ustring &file_path,
                            Glib::ustring       &error_message) {
    int res;
    std::string path = Glib::filename_from_utf8(file_path);
    memset(&vfile_, 0, sizeof(vfile_));
    if ((res = ov_fopen((char *)path.c_str(), &vfile_)) < 0) {
      switch(res) {
      case -1:            error_message = _("File open failed");                      break;
      case OV_EREAD:      error_message = _("Read error");                            break;
      case OV_ENOTVORBIS: error_message = _("File does not contain any Vorbis data"); break;
      case OV_EVERSION:   error_message = _("Vorbis version mismatch");               break;
      case OV_EBADHEADER: error_message = _("Invalid Vorbis bitstream header");       break;
      case OV_EFAULT:     error_message = _("Internal logic fault");                  break;
      default:            error_message = _("Unknown error");
      }
      return false;
    }
    return true;
  }
  virtual void prepare_format(ao_sample_format &format) {
    vorbis_info *vi = ov_info(&vfile_, -1);
    format.bits        = 16;
    format.rate        = vi->rate;
    format.channels    = vi->channels;
    format.byte_format = AO_FMT_LITTLE;
  }
  virtual long read(char *buf, size_t size) {
    long ret = ov_read(&vfile_, buf, size, 0, 2, 1, &current_section_);
    return ret;
  }
  virtual void volume(ao_sample_format &/*format*/,
                      char *buf, size_t size,
                      double volume) {
    for(size_t i = 0; i < size; i += 2) {
      int16_t sample = (buf[i] & 0xff) | ((buf[i + 1] << 8) & 0xff00);
      sample = sample * volume;
      buf[i] = sample & 0xff;
      buf[i + 1] = (sample >> 8) & 0xff;
    }
  }
  virtual void finalize() {
    ov_clear(&vfile_);
    memset(&vfile_, 0, sizeof(vfile_));
  }

public:
  OggPlayer(Application         &app,
            const Glib::ustring &file_path,
            double               volume) :
    Player          (app, file_path, volume),
    current_section_(0)
  {
    memset(&vfile_, 0, sizeof(vfile_));
  }
};
#endif

void play_thread(Player *player) {
  if (player->play(_error_message)) {
    _dispatcher_complete->emit();
  } else {
    _dispatcher_error->emit();
  }
  _mutex.lock();
  for(PlayerList::iterator iter = _players->begin(); iter != _players->end(); ++iter) {
    if (&*iter == player) {
      _players->erase(iter);
      break;
    }
  }
  _mutex.unlock();
}

void on_complete() {
  if (!_complete_cb.empty()) _complete_cb();
}
void on_error() {
  if (!_error_cb.empty()) _error_cb(_error_message);
}

void default_error_cb(const Glib::ustring &error_message) {
  log::err << error_message << log::endl;
}

uint play(const Glib::ustring &play_file,
          double               volume,
          SlotError            error_cb,
          SlotComplete         complete_cb) {
  if (play_file.empty() ||
      *_app->conf().sound()->mute() ||
      (*_app->conf().screensaver()->mute() && ss::get_active())) {
    return 0;
  }

  log::log << "Sound: " << play_file << log::endl;

  Player *player = NULL;
  const char *ext = strrchr(play_file.c_str(), '.');
  if (ext) {
    if (!strcasecmp(ext, ".wav")) {
      player = new WavPlayer(*_app, play_file, volume);
#ifdef USE_VORBIS
    } else if (!strcasecmp(ext, ".ogg")) {
      player = new OggPlayer(*_app, play_file, volume);
#endif
    }
  }
  if (player) {
    _error_cb    = error_cb ? error_cb : sigc::ptr_fun(default_error_cb);
    _complete_cb = complete_cb;
    _mutex.lock();
    _players->push_back(player);
    _mutex.unlock();
    _app->thread(sigc::bind(sigc::ptr_fun(play_thread), player));
    return player->get_id();
  } else {
    _error_message = _("unknown filename extension");
    if (error_cb) {
      error_cb(_error_message);
    } else {
      default_error_cb(_error_message);
    }
  }
  return 0;
}
void stop(uint id) {
  if (id) {
    _mutex.lock();
    for(PlayerList::iterator iter = _players->begin(); iter != _players->end(); ++iter) {
      if (iter->get_id() == id) {
        iter->stop();
        break;
      }
    }
    _mutex.unlock();
  }
}
void stop_all() {
  _mutex.lock();
  for(PlayerList::iterator iter = _players->begin(); iter != _players->end(); ++iter) iter->stop();
  _mutex.unlock();
}

void init(Application &app) {
  if (!_app) {
    _app = &app;
    _players = new PlayerList();
    _dispatcher_error = new Glib::Dispatcher();
    _dispatcher_complete = new Glib::Dispatcher();
    _dispatcher_error->connect(sigc::ptr_fun(on_error));
    _dispatcher_complete->connect(sigc::ptr_fun(on_complete));
    ao_initialize();
  }
}
void uninit() {
  _mutex.lock();
  delete _players;
  _mutex.unlock();

  delete _dispatcher_error;
  delete _dispatcher_complete;

  ao_shutdown();
}

}/*sound*/
}/*gpyp*/
